<?php

/**
 * Acl rules builder
 *
 * @author    Mon Zafra <monzee at gmail>
 * @copyright (c)2009 Mon Zafra
 * @license   http://monzee.wordpress.com/license New BSD
 * @version   $Id: AccessManager.php 38 2009-02-18 17:12:39Z monzee $
 */
class Admin_AccessManager extends Zend_Controller_Action_Helper_Abstract implements Mz_Configurable
{
    /**
     * Acl object
     *
     * @var Zend_Acl
     */
    protected $_acl;

    protected $_aclModel;
    protected $_aclModelClass = 'Admin_Acl';

    protected $_loginPage = array(
        'module' => 'admin',
        'controller' => 'user',
        'action' => 'login',
    );
    protected $_deniedPage = array(
        'module' => 'admin',
        'controller' => 'error',
        'action' => 'denied',
    );
    protected $_guestRole = 'guest';
    protected $_autoResource = true;
    protected $_resourceNameSpec = ':module/:controller';
    protected $_resourcePrivSpec = ':action';

    /**
     * For Mz_Configure
     *
     * @return bool
     */
    public function hasMagicSetter()
    {
        return true;
    }

    /**
     * Return value of protected members
     *
     * @param string $key
     * @param mixed  $default
     * @return mixed
     */
    public function getOption($key, $default = null)
    {
        $key = strtolower($key[0]) . substr($key, 1);
        $property = '_' . $key;
        return isset($this->$property) ? $this->$property : $default;
    }

    /**
     * Catch-all option setter
     *
     * @param string $option
     * @param mixed  $value
     * @return Admin_Bootstrap
     */
    public function __set($option, $value)
    {
        $property = '_' . ucfirst($option);
        if (property_exists($this, $property)) {
            $this->$property = $value;
        }
        return $this;
    }


    /**
     * Lazy load Acl object
     *
     * @return Zend_Acl
     */
    public function getAcl()
    {
        if (null === $this->_acl) {
            $this->_acl = new Zend_Acl();
        }
        return $this->_acl;
    }

    /**
     * Set Acl object
     *
     * @param Zend_Acl $acl
     * @return Admin_Bootstrap
     */
    public function setAcl(Zend_Acl $acl)
    {
        $this->_acl = $acl;
        return $this;
    }

    /**
     * The acl model is the object from which the user roles and permissions
     * are pulled from
     *
     * @return mixed
     */
    public function getAclModel()
    {
        if (null === $this->_aclModel) {
            $this->_aclModel = new $this->_aclModelClass();
        }
        return $this->_aclModel;
    }

    public function preDispatch()
    {
        if ($this->_autoResource) {
            $resource = $this->generateResource();
        } else {
            $resource = $this->getRequest()->getParams();
        }

        /* pull all roles from acl model, add to acl object
         *
         * if authenticated:
         *     pull username, roles from auth
         *     push roles to acl model (setUserRoles)
         * else:
         *     username = $this->_guestRole
         *     setUserRoles(array(guestrole => array()))
         *
         * call isAllowed(username, resource)
         * if denied, forward to login page or denied page
         */
    }

    /**
     * Generate resource id and priv
     *
     * $params must contain the keys 'module', 'controller' and 'action'
     *
     * @param array $params
     * @return array containing resourceId and privilege
     */
    public function generateResource(array $params = null)
    {
        $params = null === $params ? $this->getRequest()->getParams() : $params;
        
        $module = $params['module'];
        $controller = $params['controller'];
        $action = $params['action'];

        $replaceMap = array(
            ':module' => $module,
            ':controller' => $controller,
            ':action' => $action,
        );

        $resId = str_replace(
            array_keys($replaceMap),
            array_values($replaceMap),
            $this->_resourceNameSpec
        );

        $priv = str_replace(
            array_keys($replaceMap),
            array_values($replaceMap),
            $this->_resourcePrivSpec
        );

        return array('id' => $resId, 'privilege' => $priv);
    }

    /**
     * Set the acl model
     *
     * @param mixed $aclModel
     * @return Admin_AccessManager
     */
    public function setAclModel($aclModel)
    {
        $this->_aclModel = $aclModel;
        return $this;
    }

    /**
     * Recursively add all parent roles of a role
     *
     * @param array    $child Array of roles
     * @param Zend_Acl $acl   The acl object
     * @return array containing the names of parent roles of $role
     */
    protected function _addAllAncestorRoles($child, &$acl)
    {
        $model = $this->getAclModel();
        $parents = $model->getParentRoles($child);

        $parentRoles = array();
        foreach ($parents as $parent) {
            $parentId = $parent['id'];
            $grandParents = $this->_addAllAncestorRoles($parent, $acl);
            $acl->hasRole($parentId) or $acl->addRole(new Zend_Acl_Role($parentId), $grandParents);
            $parentRoles[] = $parentId;
        }

        return $parentRoles;
    }

    /**
     * Iterates through the array of roles and adds all the ancestors of each.
     *
     * Does not add the roles in the passed array themselves.
     *
     * @param aray     $roles Array of roles
     * @param Zend_Acl $acl   The acl object
     * @return array containing role ids of the user
     */
    protected function _addUserRoles($roles, &$acl)
    {
        $userRoles = array();
        foreach ($roles as $role) {
            $roleId = $role['id'];
            $parents = $this->_addAllAncestorRoles($role, $acl);
            $acl->hasRole($roleId) or $acl->addRole(new Zend_Acl_Role($roleId), $parents);
            $userRoles[] = $roleId;
        }

        return $userRoles;
    }

    /**
     * Adds all the roles (and all the parent roles of each role) to the acl,
     * adds the resource to the acl, builds access rules, then checks if the
     * user is allowed to access the resource.
     *
     * @param string     $username  Username of the user to check
     * @param null|array $resource  Array containing the resource id and
     *                              privilege to check OR params to pass to
     *                              resource value generator
     * @return bool Whether the user is allowed access to the resource or not
     * @todo just add all roles at once. too hard to generate a sql to get only
     *       the ancestors of a role
     * @todo push roles into the acl model somewhere then pull it here
     */
    public function isAllowed($username, $resource)
    {
        $model = $this->getAclModel();

        $roles = $model->getUserRolesByUsername($username);
        $acl = $this->getAcl();

        $userRoles = $this->_addUserRoles($roles, $acl);

        $userRoleId = md5($username);
        if (empty($roles)) {
            $acl->hasRole($userRoleId) or $acl->addRole(new Zend_Acl_Role($userRoleId));
        } else {
            $acl->hasRole($userRoleId) or $acl->addRole(new Zend_Acl_Role($userRoleId), $userRoles);
        }

        if (!isset($resource['id'])) {
            $resource = $model->getResource($resource);
        } else if (!isset($resource['privilege'])) {
            $resource['privilege'] = null;
        }

        $this->buildPermissions($resource);

        return $acl->isAllowed($userRoleId, $resource['id'], $resource['privilege']);
    }

    /**
     * Finds all rules associated with $resource and adds them to the acl object
     *
     * @param mixed $resource Resource value compatible with the acl model
     * @todo assertion classnames in the permission table?
     */
    public function buildPermissions($resource)
    {
        $acl = $this->getAcl();
        $model = $this->getAclModel();

        $resId = $resource['id'];

        $acl->has($resId) or $acl->add(new Zend_Acl_Resource($resId));

        $rules = $model->getRulesByResource($resource);

        // allow all if no rule is set for the resource
        if (empty($rules)) {
            $acl->allow(null, $resId);
        } else {
            $acl->deny(null, $resId);

            foreach ($rules as $rule) {
                $roleId = $rule['role_id'];
                $allow = isset($rule['allowed']) ? $rule['allowed'] : true;
                $priv = empty($rule['privilege']) || $rule['privilege'] == '*'
                      ? null : $rule['privilege'];

                if (!empty($roleId)) {
                    $role = $model->getRoleById($roleId);
                    if (!$acl->hasRole($roleId)) {
                        $parents = $this->_addAllAncestorRoles($role, $acl);
                        $acl->addRole(new Zend_Acl_Role($roleId), $parents);
                    }
                }

                $action = $allow ? 'allow' : 'deny';
                $acl->$action($roleId, $resId, $priv);
            }
        }
    }

}
